// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// Include test fixture.
GEN_INCLUDE([
  '//chrome/browser/resources/chromeos/accessibility/chromevox/testing/chromevox_next_e2e_test_base.js'
]);

/**
 * Test fixture for cursors.
 */
AccessibilityExtensionCursorsTest = class extends ChromeVoxNextE2ETest {
  /** Test cursors.Cursor. @const {string} */
  get CURSOR() {
    return 'cursor';
  }

  /** Test cursors.Range. @const {string} */
  get RANGE() {
    return 'range';
  }

  /** @override */
  setUp() {
    super.setUp();
    // Various aliases.
    window.BACKWARD = constants.Dir.BACKWARD;
    window.FORWARD = constants.Dir.FORWARD;
    window.CHARACTER = cursors.Unit.CHARACTER;
    window.WORD = cursors.Unit.WORD;
    window.LINE = cursors.Unit.LINE;
    window.NODE = cursors.Unit.NODE;
    window.BOUND = cursors.Movement.BOUND;
    window.DIRECTIONAL = cursors.Movement.DIRECTIONAL;
    window.SYNC = cursors.Movement.SYNC;
  }

  /**
   * Performs a series of operations on a cursor and asserts the result.
   * @param {cursors.Cursor} cursor The starting cursor.
   * @param {!Array<Array<
   *          cursors.Unit|
   *          cursors.Movement|
   *          constants.Dir|
   *          {index: number, value: string}>>} moves An array of arrays. Each
   *     inner array contains 4 items: unit, movement, direction, and assertions
   *     object. See example below.
   */
  cursorMoveAndAssert(cursor, moves) {
    let move = null;
    while (move = moves.shift()) {
      cursor = cursor.move(move[0], move[1], move[2]);
      const expected = move[3];
      this.makeCursorAssertion(expected, cursor);
    }
  }

  /**
   * Performs a series of operations on a range and asserts the result.
   * @param {cursors.Range} range The starting range.
   * @param {!Array<Array<
   *         cursors.Unit|
   *         constants.Dir|
   *         {index: number, value: string}>>} moves An array of arrays. Each
   *     inner array contains 4 items: unit, direction, start and end assertions
   *     objects. See example below.
   */
  rangeMoveAndAssert(range, moves) {
    let move = null;
    while (move = moves.shift()) {
      range = range.move(move[0], move[1]);
      const expectedStart = move[2];
      const expectedEnd = move[3];

      this.makeCursorAssertion(expectedStart, range.start);
      this.makeCursorAssertion(expectedEnd, range.end);
    }
  }

  /**
   * Makes assertions about the given |cursor|.
   * @param {{index: number, value: string}} expected
   * @param {Cursor} cursor
   */
  makeCursorAssertion(expected, cursor) {
    if (goog.isDef(expected.value)) {
      assertEquals(expected.value, cursor.node.name);
    }
    if (goog.isDef(expected.index)) {
      assertEquals(expected.index, cursor.index,
        'wrong index at ' + expected.value + ', expected: ' + expected.index + ' actual: ' + cursor.index);
    }
  }

  /**
   * Runs the specified moves on the |doc| and asserts expectations.
   * @param {function} doc
   * @param {!Array<Array<
   *          cursors.Unit|
   *          cursors.Movement|
   *          constants.Dir|
   *          {index: number, value: string}>>} moves An array of arrays. Each
   *     inner array contains 4 items: see |cursorMoveAndAssert| and
   *     |rangeMoveAndAssert|.
   * @param {string=} opt_testType Either CURSOR or RANGE.
   */
  runCursorMovesOnDocument(doc, moves, opt_testType) {
    this.runWithLoadedTree(doc, function(root) {
      let start = null;

      // This occurs as a result of a load complete.
      start =
          AutomationUtil.findNodePost(root, FORWARD, AutomationPredicate.leaf);

      const cursor = new cursors.Cursor(start, 0);
      if (!opt_testType || opt_testType === this.CURSOR) {
        const cursor = new cursors.Cursor(start, 0);
        this.cursorMoveAndAssert(cursor, moves);
      } else if (opt_testType === this.RANGE) {
        const range = new cursors.Range(cursor, cursor);
        this.rangeMoveAndAssert(range, moves);
      }
    });
  }

  get simpleDoc() {
    return `
      <p>start <span>same line</span>
      <p>end
    `;
  }

  get multiInlineDoc() {
    return `
      <p style='max-width: 5px'>start diff line</p>
      <p>end
    `;
  }

  get buttonAndInlineTextDoc() {
    return `
      <div>Inline text content</div>
      <div role="button">Button example content</div>
    `;
  }
};


TEST_F('AccessibilityExtensionCursorsTest', 'CharacterCursor', function() {
  this.runCursorMovesOnDocument(this.simpleDoc, [
    [CHARACTER, DIRECTIONAL, FORWARD, {index: 1, value: 'start '}],
    [CHARACTER, DIRECTIONAL, BACKWARD, {index: 0, value: 'start '}],
    // Bumps up against edge.
    [CHARACTER, DIRECTIONAL, BACKWARD, {index: 0, value: 'start '}],

    [CHARACTER, DIRECTIONAL, FORWARD, {index: 1, value: 'start '}],
    [CHARACTER, DIRECTIONAL, FORWARD, {index: 2, value: 'start '}],
    [CHARACTER, DIRECTIONAL, FORWARD, {index: 3, value: 'start '}],
    [CHARACTER, DIRECTIONAL, FORWARD, {index: 4, value: 'start '}],
    [CHARACTER, DIRECTIONAL, FORWARD, {index: 5, value: 'start '}],

    [CHARACTER, DIRECTIONAL, FORWARD, {index: 0, value: 'same line'}],
    [CHARACTER, DIRECTIONAL, FORWARD, {index: 1, value: 'same line'}],
    [CHARACTER, DIRECTIONAL, BACKWARD, {index: 0, value: 'same line'}],

    [CHARACTER, DIRECTIONAL, BACKWARD, {index: 5, value: 'start '}],
  ]);
});

TEST_F('AccessibilityExtensionCursorsTest', 'WordCursor', function() {
  this.runCursorMovesOnDocument(this.simpleDoc, [
    // Word (BOUND).
    [WORD, BOUND, BACKWARD, {index: 0, value: 'start '}],
    [WORD, BOUND, BACKWARD, {index: 0, value: 'start '}],
    [WORD, BOUND, FORWARD, {index: 5, value: 'start '}],
    [WORD, BOUND, FORWARD, {index: 5, value: 'start '}],

    // Word (DIRECTIONAL).
    [WORD, DIRECTIONAL, FORWARD, {index: 0, value: 'same line'}],
    [WORD, DIRECTIONAL, FORWARD, {index: 5, value: 'same line'}],

    [WORD, DIRECTIONAL, FORWARD, {index: 0, value: 'end'}],
    [WORD, DIRECTIONAL, FORWARD, {index: 0, value: 'end'}],

    [WORD, DIRECTIONAL, BACKWARD, {index: 5, value: 'same line'}],
    [WORD, DIRECTIONAL, BACKWARD, {index: 0, value: 'same line'}],

    [WORD, DIRECTIONAL, BACKWARD, {index: 0, value: 'start '}],
    [WORD, DIRECTIONAL, BACKWARD, {index: 0, value: undefined}]
  ]);
});

TEST_F('AccessibilityExtensionCursorsTest', 'CharacterWordCursor', function() {
  this.runCursorMovesOnDocument(this.simpleDoc, [
    [CHARACTER, DIRECTIONAL, FORWARD, {index: 1, value: 'start '}],

    [WORD, DIRECTIONAL, FORWARD, {index: 0, value: 'same line'}],
    [CHARACTER, DIRECTIONAL, FORWARD, {index: 1, value: 'same line'}],
    [WORD, DIRECTIONAL, FORWARD, {index: 5, value: 'same line'}],
    [CHARACTER, DIRECTIONAL, BACKWARD, {index: 4, value: 'same line'}],
    [WORD, DIRECTIONAL, FORWARD, {index: 5, value: 'same line'}],
    [CHARACTER, DIRECTIONAL, FORWARD, {index: 6, value: 'same line'}],
    [WORD, DIRECTIONAL, BACKWARD, {index: 0, value: 'same line'}],
    [CHARACTER, DIRECTIONAL, BACKWARD, {index: 5, value: 'start '}],
    [CHARACTER, DIRECTIONAL, BACKWARD, {index: 4, value: 'start '}],
    [WORD, DIRECTIONAL, BACKWARD, {index: 0, value: 'start '}]
  ]);
});

TEST_F('AccessibilityExtensionCursorsTest', 'LineCursor', function() {
  this.runCursorMovesOnDocument(this.simpleDoc, [
    // Line (BOUND).
    [LINE, BOUND, FORWARD, {value: 'same line'}],
    [LINE, BOUND, FORWARD, {value: 'same line'}],
    [LINE, BOUND, BACKWARD, {value: 'start '}],
    [LINE, BOUND, BACKWARD, {value: 'start '}],

    // Line (DIRECTIONAL).
    [LINE, DIRECTIONAL, FORWARD, {value: 'end'}],
    [LINE, DIRECTIONAL, FORWARD, {value: 'end'}],
    [LINE, DIRECTIONAL, BACKWARD, {value: 'same line'}],
    // Bump against an edge.
    [LINE, DIRECTIONAL, BACKWARD, {value: 'same line'}],
    [LINE, BOUND, BACKWARD, {value: 'start '}]
  ]);
});

TEST_F('AccessibilityExtensionCursorsTest', 'SyncCursor', function() {
  this.runCursorMovesOnDocument(this.simpleDoc, [
    [WORD, SYNC, FORWARD, {index: 0, value: 'start '}],

    [NODE, DIRECTIONAL, FORWARD, {value: 'same line'}],
    [CHARACTER, SYNC, FORWARD, {index: 0, value: 'same line'}],

    [NODE, DIRECTIONAL, BACKWARD, {value: 'start '}],
    [CHARACTER, SYNC, BACKWARD, {index: 5, value: 'start '}],

    [NODE, DIRECTIONAL, FORWARD, {value: 'same line'}],
    [WORD, SYNC, BACKWARD, {index: 5, value: 'same line'}],
  ]);
});

TEST_F('AccessibilityExtensionCursorsTest', 'CharacterRange', function() {
  this.runCursorMovesOnDocument(
      this.simpleDoc,
      [
        [
          CHARACTER, FORWARD, {value: 'start ', index: 1},
          {value: 'start ', index: 2}
        ],
        [
          CHARACTER, FORWARD, {value: 'start ', index: 2},
          {value: 'start ', index: 3}
        ],
        [
          CHARACTER, FORWARD, {value: 'start ', index: 3},
          {value: 'start ', index: 4}
        ],
        [
          CHARACTER, FORWARD, {value: 'start ', index: 4},
          {value: 'start ', index: 5}
        ],
        [
          CHARACTER, FORWARD, {value: 'start ', index: 5},
          {value: 'start ', index: 6}
        ],

        [
          CHARACTER, FORWARD, {value: 'same line', index: 0},
          {value: 'same line', index: 1}
        ],

        [
          CHARACTER, BACKWARD, {value: 'start ', index: 5},
          {value: 'start ', index: 6}
        ],
        [
          CHARACTER, BACKWARD, {value: 'start ', index: 4},
          {value: 'start ', index: 5}
        ],
        [
          CHARACTER, BACKWARD, {value: 'start ', index: 3},
          {value: 'start ', index: 4}
        ],
        [
          CHARACTER, BACKWARD, {value: 'start ', index: 2},
          {value: 'start ', index: 3}
        ],
        [
          CHARACTER, BACKWARD, {value: 'start ', index: 1},
          {value: 'start ', index: 2}
        ],
        [
          CHARACTER, BACKWARD, {value: 'start ', index: 0},
          {value: 'start ', index: 1}
        ],
        [
          CHARACTER, BACKWARD, {value: undefined, index: 0},
          {value: undefined, index: 1}
        ],
      ],
      this.RANGE);
});

TEST_F('AccessibilityExtensionCursorsTest', 'WordRange', function() {
  this.runCursorMovesOnDocument(
      this.simpleDoc,
      [
        [
          WORD, FORWARD, {value: 'same line', index: 0},
          {value: 'same line', index: 4}
        ],
        [
          WORD, FORWARD, {value: 'same line', index: 5},
          {value: 'same line', index: 9}
        ],

        [WORD, FORWARD, {value: 'end', index: 0}, {value: 'end', index: 3}],
        [WORD, FORWARD, {value: 'end', index: 0}, {value: 'end', index: 3}],

        [
          WORD, BACKWARD, {value: 'same line', index: 5},
          {value: 'same line', index: 9}
        ],
        [
          WORD, BACKWARD, {value: 'same line', index: 0},
          {value: 'same line', index: 4}
        ],

        [
          WORD, BACKWARD, {value: 'start ', index: 0},
          {value: 'start ', index: 5}
        ],
        [
          WORD, BACKWARD, {value: undefined, index: 0},
          {value: undefined, index: 5}
        ],
      ],
      this.RANGE);
});


TEST_F('AccessibilityExtensionCursorsTest', 'LineRange', function() {
  this.runCursorMovesOnDocument(
      this.simpleDoc,
      [
        [LINE, FORWARD, {value: 'end', index: 0}, {value: 'end', index: 3}],
        [LINE, FORWARD, {value: 'end', index: 0}, {value: 'end', index: 3}],
        [LINE, FORWARD, {value: 'end', index: 0}, {value: 'end', index: 3}],
        [LINE, FORWARD, {value: 'end', index: 0}, {value: 'end', index: 3}],

        [
          LINE, BACKWARD, {value: 'start ', index: 0},
          {value: 'same line', index: 9}
        ],

        [
          LINE, BACKWARD, {value: 'start ', index: 0},
          {value: 'same line', index: 9}
        ]
      ],
      this.RANGE);
});

TEST_F(
    'AccessibilityExtensionCursorsTest', 'DontSplitOnNodeNavigation',
    function() {
      this.runWithLoadedTree(this.multiInlineDoc, function(root) {
        const para = root.firstChild;
        assertEquals('paragraph', para.role);
        let cursor = new cursors.Cursor(para.firstChild, 0);
        cursor = cursor.move(NODE, DIRECTIONAL, FORWARD);
        assertEquals('staticText', cursor.node.role);
        assertEquals('end', cursor.node.name);

        cursor = cursor.move(NODE, DIRECTIONAL, BACKWARD);
        assertEquals('staticText', cursor.node.role);
        assertEquals('start diff line', cursor.node.name);

        assertEquals('inlineTextBox', cursor.node.firstChild.role);
        assertEquals('start ', cursor.node.firstChild.name);
        assertEquals('diff ', cursor.node.firstChild.nextSibling.name);
        assertEquals('line', cursor.node.lastChild.name);
      });
    });

TEST_F('AccessibilityExtensionCursorsTest', 'WrappingCursors', function() {
  this.runWithLoadedTree(this.multiInlineDoc, function(root) {
    const first = root;
    const last = root.lastChild.firstChild;
    let cursor = new cursors.WrappingCursor(first, -1);

    // Wrap from first node to last node.
    cursor = cursor.move(NODE, DIRECTIONAL, BACKWARD);
    assertEquals(last, cursor.node);

    // Wrap from last node to first node.
    cursor = cursor.move(NODE, DIRECTIONAL, FORWARD);
    assertEquals(first, cursor.node);
  });
});

TEST_F('AccessibilityExtensionCursorsTest', 'IsInWebRange', function() {
  this.runWithLoadedTree(this.simpleDoc, function(root) {
    const para = root.firstChild;
    const webRange = cursors.Range.fromNode(para);
    const auraRange = cursors.Range.fromNode(root.parent);
    assertFalse(auraRange.isWebRange());
    assertTrue(webRange.isWebRange());
  });
});

// Disabled due to being flaky on ChromeOS. See https://crbug.com/1227435.
TEST_F(
    'AccessibilityExtensionCursorsTest', 'DISABLED_SingleDocSelection',
    function() {
      this.runWithLoadedTree(
          `
    <span>start</span>
    <p><a href="google.com">google home page</a></p>
    <p>some more text</p>
    <p>end of text</p>
  `,
          function(root) {
            // For some reason, Blink fails if we don't first select something
            // on the page.
            ChromeVoxState.instance.currentRange.select();
            const link = root.find({role: RoleType.LINK});
            const p1 = root.find({role: RoleType.PARAGRAPH});
            const p2 = p1.nextSibling;

            const singleSel = new cursors.Range(
                new cursors.Cursor(link, 0), new cursors.Cursor(link, 1));

            const multiSel = new cursors.Range(
                new cursors.Cursor(p1.firstChild, 2),
                new cursors.Cursor(p2.firstChild, 4));

            function verifySel() {
              if (root.selectionStartObject === link.firstChild) {
                assertEquals(link.firstChild, root.selectionStartObject);
                assertEquals(0, root.selectionStartOffset);
                assertEquals(link.firstChild, root.selectionEndObject);
                assertEquals(1, root.selectionEndOffset);
                this.listenOnce(root, 'textSelectionChanged', verifySel);
                multiSel.select();
              } else if (root.selectionStartObject === p1.firstChild) {
                assertEquals(p1.firstChild, root.selectionStartObject);
                assertEquals(2, root.selectionStartOffset);
                assertEquals(p2.firstChild, root.selectionEndObject);
                assertEquals(4, root.selectionEndOffset);
              }
            }

            this.listenOnce(root, 'textSelectionChanged', verifySel, true);
            singleSel.select();
          });
    });

TEST_F(
    'AccessibilityExtensionCursorsTest', 'MultiLineOffsetSelection',
    function() {
      this.runWithLoadedTree(this.multiInlineDoc, function(root) {
        const secondLine = root.firstChild.firstChild.firstChild.nextSibling;
        assertEquals('inlineTextBox', secondLine.role);
        assertEquals('diff ', secondLine.name);

        let secondLineCursor = new cursors.Cursor(secondLine, -1);
        // The selected node moves to the static text node.
        assertEquals(
            secondLineCursor.node.parent, secondLineCursor.selectionNode);

        // This selects the entire node via a character offset.
        assertEquals(6, secondLineCursor.selectionIndex);

        // Index into the characters.
        secondLineCursor = new cursors.Cursor(secondLine, 1);
        assertEquals(7, secondLineCursor.selectionIndex);

        // Now, try selecting via node offsets.
        let cursor = new cursors.Cursor(root.firstChild, -1);
        assertEquals(root, cursor.selectionNode);
        assertEquals(0, cursor.selectionIndex);

        cursor = new cursors.Cursor(root.firstChild.nextSibling, -1);
        assertEquals(root, cursor.selectionNode);
        assertEquals(1, cursor.selectionIndex);
      });
    });

TEST_F('AccessibilityExtensionCursorsTest', 'InlineElementOffset', function() {
  this.runWithLoadedTree(
      `
    <span>start</span>
    <p>This<br> is a<a href="#g">test</a>of selection</p>
  `,
      function(root) {
        root.addEventListener(
            'textSelectionChanged', this.newCallback(function(evt) {
              // Test setup moves initial focus; ensure we don't test that here.
              if (testNode !== root.selectionStartObject) {
                return;
              }

              // This is a little unexpected though not really incorrect; Ctrl+C
              // works.
              assertEquals(testNode, root.selectionStartObject);
              assertEquals(ofSelectionNode, root.selectionEndObject);
              assertEquals(4, root.selectionStartOffset);
              assertEquals(1, root.selectionEndOffset);
            }));

        // This is the link's static text.
        const testNode = root.lastChild.lastChild.previousSibling.firstChild;
        assertEquals(RoleType.STATIC_TEXT, testNode.role);
        assertEquals('test', testNode.name);

        const ofSelectionNode = root.lastChild.lastChild;
        const cur = new cursors.Cursor(ofSelectionNode, 0);
        assertEquals('of selection', cur.selectionNode.name);
        assertEquals(RoleType.STATIC_TEXT, cur.selectionNode.role);
        assertEquals(0, cur.selectionIndex);

        const curIntoO = new cursors.Cursor(ofSelectionNode, 1);
        assertEquals('of selection', curIntoO.selectionNode.name);
        assertEquals(RoleType.STATIC_TEXT, curIntoO.selectionNode.role);
        assertEquals(1, curIntoO.selectionIndex);

        const oRange = new cursors.Range(cur, curIntoO);
        oRange.select();
      });
});

TEST_F('AccessibilityExtensionCursorsTest', 'ContentEquality', function() {
  this.runWithLoadedTree(
      `
    <div role="region" aria-label="test region">this is a test</button>
  `,
      function(root) {
        const region = root.firstChild;
        assertEquals(RoleType.REGION, region.role);
        const staticText = region.firstChild;
        assertEquals(RoleType.STATIC_TEXT, staticText.role);
        const inlineTextBox = staticText.firstChild;
        assertEquals(RoleType.INLINE_TEXT_BOX, inlineTextBox.role);

        const rootRange = cursors.Range.fromNode(root);
        const regionRange = cursors.Range.fromNode(region);
        const staticTextRange = cursors.Range.fromNode(staticText);
        const inlineTextBoxRange = cursors.Range.fromNode(inlineTextBox);

        // Positive cases.
        assertTrue(regionRange.contentEquals(staticTextRange));
        assertTrue(staticTextRange.contentEquals(regionRange));
        assertTrue(inlineTextBoxRange.contentEquals(staticTextRange));
        assertTrue(staticTextRange.contentEquals(inlineTextBoxRange));

        // Negative cases.
        assertFalse(rootRange.contentEquals(regionRange));
        assertFalse(rootRange.contentEquals(staticTextRange));
        assertFalse(rootRange.contentEquals(inlineTextBoxRange));
        assertFalse(regionRange.contentEquals(rootRange));
        assertFalse(staticTextRange.contentEquals(rootRange));
        assertFalse(inlineTextBoxRange.contentEquals(rootRange));
      });
});

TEST_F('AccessibilityExtensionCursorsTest', 'DeepEquivalency', function() {
  this.runWithLoadedTree(
      `
    <p style="word-spacing:100000px">this is a test</p>
  `,
      function(root) {
        const textNode = root.find({role: RoleType.STATIC_TEXT});

        let text = new cursors.Cursor(textNode, 2);
        deep = text.deepEquivalent;
        assertEquals('this ', deep.node.name);
        assertEquals(RoleType.INLINE_TEXT_BOX, deep.node.role);
        assertEquals(2, deep.index);

        text = new cursors.Cursor(textNode, 5);
        deep = text.deepEquivalent;
        assertEquals('is ', deep.node.name);
        assertEquals(RoleType.INLINE_TEXT_BOX, deep.node.role);
        assertEquals(0, deep.index);

        text = new cursors.Cursor(textNode, 7);
        deep = text.deepEquivalent;
        assertEquals('is ', deep.node.name);
        assertEquals(RoleType.INLINE_TEXT_BOX, deep.node.role);
        assertEquals(2, deep.index);

        text = new cursors.Cursor(textNode, 8);
        deep = text.deepEquivalent;
        assertEquals('a ', deep.node.name);
        assertEquals(RoleType.INLINE_TEXT_BOX, deep.node.role);
        assertEquals(0, deep.index);

        text = new cursors.Cursor(textNode, 11);
        deep = text.deepEquivalent;
        assertEquals('test', deep.node.name);
        assertEquals(RoleType.INLINE_TEXT_BOX, deep.node.role);
        assertEquals(1, deep.index);

        // This is the only selection that can be placed at the length of the
        // node's text. This only happens at the end of a line.
        text = new cursors.Cursor(textNode, 14);
        deep = text.deepEquivalent;
        assertEquals('test', deep.node.name);
        assertEquals(RoleType.INLINE_TEXT_BOX, deep.node.role);
        assertEquals(4, deep.index);

        // However, any offset larger is invalid.
        text = new cursors.Cursor(textNode, 15);
        deep = text.deepEquivalent;
        assertTrue(text.equals(deep));
      });
});

TEST_F(
    'AccessibilityExtensionCursorsTest', 'DeepEquivalencyBeyondLastChild',
    function() {
      this.runWithLoadedTree(
          `
    <p>test</p>
  `,
          function(root) {
            const paragraph = root.find({role: RoleType.PARAGRAPH});
            assertEquals(1, paragraph.children.length);
            const cursor = new cursors.Cursor(paragraph, 1);

            const deep = cursor.deepEquivalent;
            assertEquals(RoleType.STATIC_TEXT, deep.node.role);
            assertEquals(4, deep.index);
          });
    });

TEST_F(
    'AccessibilityExtensionCursorsTest', 'SelectionAdjustmentsRichText',
    function() {
      this.runWithLoadedTree(
          `
    <div contenteditable><p>test</p><p>123</p></div>
  `,
          function(root) {
            const textField = root.firstChild;
            const paragraph = textField.firstChild;
            const otherParagraph = textField.lastChild;
            const staticText = paragraph.firstChild;
            const otherStaticText = otherParagraph.firstChild;

            // Ranges by default surround a node. Ensure it results in a
            // collapsed selection.
            let range = cursors.Range.fromNode(staticText);
            assertEquals(0, range.start.selectionIndex);
            assertEquals(0, range.end.selectionIndex);
            assertEquals(paragraph, range.start.selectionNode);
            assertEquals(paragraph, range.end.selectionNode);

            // Text selection.
            range = new cursors.Range(
                new cursors.Cursor(staticText, 2),
                new cursors.Cursor(staticText, 4));
            assertEquals(2, range.start.selectionIndex);
            assertEquals(4, range.end.selectionIndex);
            assertEquals(staticText, range.start.selectionNode);
            assertEquals(staticText, range.end.selectionNode);

            // Tree selection.
            range = cursors.Range.fromNode(paragraph);
            assertEquals(0, range.start.selectionIndex);
            assertEquals(0, range.end.selectionIndex);
            assertEquals(textField, range.start.selectionNode);
            assertEquals(textField, range.end.selectionNode);

            range = cursors.Range.fromNode(otherStaticText);
            assertEquals(0, range.start.selectionIndex);
            assertEquals(0, range.end.selectionIndex);
            assertEquals(otherParagraph, range.start.selectionNode);
            assertEquals(otherParagraph, range.end.selectionNode);

            range = cursors.Range.fromNode(otherParagraph);
            assertEquals(1, range.start.selectionIndex);
            assertEquals(1, range.end.selectionIndex);
            assertEquals(textField, range.start.selectionNode);
            assertEquals(textField, range.end.selectionNode);
          });
    });

TEST_F(
    'AccessibilityExtensionCursorsTest', 'SelectionAdjustmentsNonRichText',
    function() {
      this.runWithLoadedTree(
          `
    <input type="text"></input>
    <textarea></textarea>
  `,
          function(root) {
            const testEditable = function(edit) {
              // Occurs as part of ordinary (non-text) navigation.
              let range = cursors.Range.fromNode(edit);
              assertEquals(-1, range.start.selectionIndex);
              assertEquals(-1, range.end.selectionIndex);
              assertEquals(edit, range.start.selectionNode);
              assertEquals(edit, range.end.selectionNode);

              // Occurs as a result of explicit text nav e.g. nextCharacter
              // command.
              range = new cursors.Range(
                  new cursors.Cursor(edit, 2), new cursors.Cursor(edit, 3));
              assertEquals(2, range.start.selectionIndex);
              assertEquals(3, range.end.selectionIndex);
              assertEquals(edit, range.start.selectionNode);
              assertEquals(edit, range.end.selectionNode);
            };

            const textField = root.firstChild.firstChild;
            const textArea = root.lastChild.lastChild;

            // Both of these should behave in the same way.
            testEditable(textField);
            testEditable(textArea);
          });
    });

TEST_F(
    'AccessibilityExtensionCursorsTest', 'MovementByWordThroughNonInlineText',
    function() {
      this.runCursorMovesOnDocument(this.buttonAndInlineTextDoc, [
        // Move forward by word.
        // 'text' start and end indices.
        [WORD, DIRECTIONAL, FORWARD, {index: 7, value: 'Inline text content'}],
        [WORD, BOUND, FORWARD, {index: 11, value: 'Inline text content'}],
        // 'content' start and end incies.
        [WORD, DIRECTIONAL, FORWARD, {index: 12, value: 'Inline text content'}],
        [WORD, BOUND, FORWARD, {index: 19, value: 'Inline text content'}],
        // 'Button' start and end indices.
        [
          WORD, DIRECTIONAL, FORWARD,
          {index: 0, value: 'Button example content'}
        ],
        [WORD, BOUND, FORWARD, {index: 6, value: 'Button example content'}],
        // 'example' start and end indices.
        [
          WORD, DIRECTIONAL, FORWARD,
          {index: 7, value: 'Button example content'}
        ],
        [WORD, BOUND, FORWARD, {index: 14, value: 'Button example content'}],
        // 'content' start index. Reached last word of last object.
        [
          WORD, DIRECTIONAL, FORWARD,
          {index: 15, value: 'Button example content'}
        ],
        [
          WORD, DIRECTIONAL, FORWARD,
          {index: 15, value: 'Button example content'}
        ],

        // Move backward by word.
        // Only test start indices.
        [
          WORD, DIRECTIONAL, BACKWARD,
          {index: 7, value: 'Button example content'}
        ],
        [
          WORD, DIRECTIONAL, BACKWARD,
          {index: 0, value: 'Button example content'}
        ],
        [
          WORD, DIRECTIONAL, BACKWARD, {index: 12, value: 'Inline text content'}
        ],
        [WORD, DIRECTIONAL, BACKWARD, {index: 7, value: 'Inline text content'}],
        [WORD, DIRECTIONAL, BACKWARD, {index: 0, value: 'Inline text content'}],
        // Reached first word of first object.
        [WORD, DIRECTIONAL, BACKWARD, {index: 0, value: 'Inline text content'}]
      ]);
    });
